Cache, invalidation and reload
INTRODUCTION
diginsight SmartCache
provides hybrid, distributed, multilevel caching based on age sensitive data management.
This article discusses how we can use SmartCache
to:
- Cache data from a call and associate a suitable key to it
- Invalidate entries
- Reload cache entries upon invalidation
The code snippets and images below are taken from the SampleWebAPI working samples within the smartcache.samples repository.
STEP 01 - Cache data from a call and associate a suitable key to it
Let’s assume we need to cache data from a long latency operation such as:
public async Task<Plant> GetPlantByIdImplAsync([FromRoute] Guid id)
{
using var activity = Program.ActivitySource.StartMethodActivity(logger, new { id });
var result = default(IEnumerable<Plant>);
// ... implementation ...
?.SetOutput(plant);
activityreturn plant;
}
The result from the method executions can be cached with the following steps:
inject
smartCache
andcacheKeyService
into the current class
create a new method
GetPlantByIdAsync
callingGetPlantByIdImplAsync
by means of smartCache servicepublic async Task<Plant> GetPlantByIdAsync([FromRoute] Guid plantId) { using var activity = Program.ActivitySource.StartMethodActivity(logger); var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) }; var cacheKey = new MethodCallCacheKey(cacheKeyService, typeof(PlantsController), nameof(GetPlantByIdAsync), plantId); var plant = await smartCache.GetAsync(cacheKey, _ => GetPlantByIdImplAsync(plantId), options); ?.SetOutput(plant); activityreturn plant; }
the
smartCache.GetAsyc()
call manages cached call toGetPlantByIdImplAsync
:- cacheKey: is the cache key class associated to the cache entry
- delegate with the call to
GetPlantByIdImplAsync
is used to fetch values in case of cache miss - (opt) options with required MaxAge allows requesting data specifying a specific age criteria: cache hit will happen only if available data is within the requested age.
- cacheKey: is the cache key class associated to the cache entry
calling GetPlantByIdAsync
twice, you will get the following log where:
- the first call is a cache miss with latency of >1sec
- the first call is a cache hit with latency of few ms
STEP 02 - Add Invalidation support to cached calls
Assume you are calling a cached method with the following code
public async Task<IEnumerable<Plant>> GetPlantsAsync()
{
using var activity = Program.ActivitySource.StartMethodActivity(logger);
var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) };
var cacheKey = new GetPlantByIdCacheKey(Guid.Empty);
<IEnumerable<Plant>> getCachedValuesAsync() =>
Task.GetAsync(cacheKey, _ => GetPlantsImplAsync(), options);
smartCache
var plants = await getCachedValuesAsync();
?.SetOutput(plants);
activityreturn plants;
}
You can add support to invalidation deriving your key from IInvalidatable
:
internal sealed record GetPlantByIdCacheKey(Guid PlantId) : IInvalidatable
{
public bool IsInvalidatedBy(IInvalidationRule invalidationRule, out Func<Task> ic)
{
= null;
ic if (invalidationRule is PlantInvalidationRule pir && (PlantId == Guid.Empty || pir.PlantId == PlantId))
{
return true;
}
return false;
}
}
Upon plant creation/update/delete, you can trigger invalidation by means of smartCache.Invalidate();
call:
When Invalidating a plantId, all keys will be enumerated and those invalidated by the ID will be dismissed by the cache.
Calling GetPlantsAsync
after an update to the Plant, you will get a cache miss as the entry associated to the call has been dismissed.
The image below shows that: - after updating a Plant - the GetPlantsAsync
call gets a cache miss call as its cache entry has been invalidated
STEP 03 - Add automatic reload support to cached calls
Assume you are calling a cached method with the following code
public async Task<IEnumerable<Plant>> GetPlantsAsync()
{
using var activity = Program.ActivitySource.StartMethodActivity(logger);
var options = new SmartCacheOperationOptions() { MaxAge = TimeSpan.FromMinutes(10) };
var cacheKey = new GetPlantByIdCacheKey(cacheKeyService, Guid.Empty);
<IEnumerable<Plant>> getCachedValuesAsync() =>
Task.GetAsync(cacheKey, _ => GetPlantsImplAsync(), options);
smartCache.ReloadAsync = getCachedValuesAsync;
cacheKey
var plants = await getCachedValuesAsync();
?.SetOutput(plants);
activityreturn plants;
}
In this case, getCachedValuesAsync
delegate is used to load data.
Also, getCachedValuesAsync
is assigned to cacheKey.ReloadAsync
property to enable cache entry reload, after invalidation.
When Invalidating a plantId, all keys will be enumerated and those invalidated by the ID will be dismissed by the cache.
If ReloadAsync
delegate is available, after invalidation, the delegate is invoked to load the cache entry again.
Calling GetPlantsAsync
after an update to the Plant, this time you will get a cache hit as the entry associated to the call has been reloaded after invalidation.
The image below shows that:
- after updating a Plant
- the
GetPlantsAsync
call gets a cache hit call as its cache entry has been reloaded, after invalidation